#! python
#! coding: utf-8

__author__ = 'Anton Vakhrushev'

import os
import wx
import abc
import StringIO
import datetime
import imghash
import cachedb
from lib.extra import utils


class PhotoError(Exception):
    pass


class BasePhoto(object):
    __metaclass__ = abc.ABCMeta

    @staticmethod
    def _load_cache(sid, remote, mtime):
        now = datetime.datetime.now()
        try:
            cache = cachedb.Photo.get(cachedb.Photo.sid == sid, cachedb.Photo.remote == remote)
            if mtime > cache.modified:
                cache.delete_instance()
                cache = cachedb.Photo.create(sid=sid, remote=remote, viewed=now, modified=mtime)
            else:
                cache.viewed = now
                cache.save()
        except cachedb.Photo.DoesNotExist:
            cache = cachedb.Photo.create(sid=sid, remote=remote, viewed=now, modified=mtime)
        return cache

    def __init__(self):
        self._hash = None
        self._cache = None
        self._title = ''
        self._ratio = None

    @abc.abstractmethod
    def _get_img_hash(self):
        return None

    def get_title(self):
        return self._title

    def get_ratio(self):
        return self._ratio

    def get_hash(self):
        if self._hash is not None:
            return self._hash
        hash_values = [i.value for i in self._cache.hash_values.order_by(cachedb.HashValue.sort)]
        if hash_values:
            self._hash = imghash.ImageHash(hash_values)
        else:
            self.update_hash(self._get_img_hash())
        assert self._hash is not None
        return self._hash

    def update_hash(self, img_hash):
        rows = ({'photo': self._cache, 'sort': idx, 'value': value} for idx, value in enumerate(img_hash.values))
        with cachedb.transaction():
            cachedb.HashValue.delete().where(cachedb.HashValue.photo == self._cache).execute()
            cachedb.HashValue.insert_many(rows).execute()
        self._hash = img_hash


class RemotePhoto(BasePhoto):
    def __init__(self, yaphoto):
        """
        @type yaphoto: yafotki.YandexPhoto
        """
        BasePhoto.__init__(self)
        self._yaphoto = yaphoto
        self._title = self._yaphoto.title
        last_img = yaphoto.images[-1]
        self._ratio = float(last_img.w) / float(last_img.h)
        self._mtime = self._yaphoto.edited
        self._ctime = self._yaphoto.published
        self._cache = self._load_cache(sid=yaphoto.id, remote=True, mtime=self._mtime)

    def _get_img_hash(self):
        data = self._get_data(800, 800)
        return imghash.ImageHash.from_stream(data)

    def _get_data(self, w, h):
        img = self.nearest(w, h)
        now = datetime.datetime.now()
        try:
            img_data = cachedb.Image.get(cachedb.Image.link == img.href)
            data = img_data.data
            img_data.viewed = now
            img_data.save()
        except cachedb.Image.DoesNotExist:
            data = img.download()
            cachedb.Image.create(photo=self._cache, link=img.href, data=data, viewed=now)
        if img.w > self._cache.width or img.h > self._cache.height:
            self._cache.width = img.w
            self._cache.height = img.h
            if img.h:
                self._cache.ratio = float(img.w) / float(img.h)
            self._cache.save()
            if self._hash:
                self.update_hash(imghash.ImageHash.from_stream(data))
        return data

    def get_id(self):
        return self._yaphoto.id

    def get_size(self):
        last_img = self._yaphoto.images[-1]
        return int(last_img.size) if last_img.size else None

    def get_create_time(self):
        return self._ctime

    def get_capture_time(self):
        return self._yaphoto.created

    def wximage(self, w, h):
        data = self._get_data(w, h)
        return wx.ImageFromStream(StringIO.StringIO(data))

    def nearest(self, w, h, orig=False):
        """
        Возвращает версию фотографии не менее, чем прямоугольник w, h
        @rtype : yafotki.YandexImage
        """
        if not orig:
            images = [i for i in self._yaphoto.images if i.name != u'orig']
        else:
            images = self._yaphoto.images
        if not images:
            raise ValueError('There is no images')
        try:
            return next((i for i in images if i.w >= w or i.h >= h))
        except StopIteration:
            return images[-1]

    def delete(self):
        self._yaphoto.delete()


class LocalPhoto(BasePhoto):
    def __init__(self, local_path):
        BasePhoto.__init__(self)
        if not os.path.exists(local_path):
            raise PhotoError('File "{}" not found'.format(local_path))
        self._path = local_path
        self._title = os.path.split(self._path)[1]
        self._mtime = datetime.datetime.fromtimestamp(os.path.getmtime(self._path))
        self._cache = self._load_cache(sid=self._path, remote=False, mtime=self._mtime)

    def _get_img_hash(self):
        return imghash.ImageHash.from_file(self._path)

    def get_path(self):
        return self._path

    def get_size(self):
        return os.path.getsize(self._path)

    def get_edited_time(self):
        return self._mtime

    def get_capture_time(self):
        return None

    def wximage(self, w, h):
        return utils.resize(wx.Image(self._path), w, h)


class MatchedPhoto(object):
    def __init__(self, remote=None, local=None):
        if not remote and not local:
            raise PhotoError('Empty photo')
        self.__remote = remote and RemotePhoto(remote)
        self.__local = local and LocalPhoto(local)

    def is_local(self, strict=False):
        return bool(self.__local and (not self.__remote if strict else True))

    def is_remote(self, strict=False):
        return bool(self.__remote and (not self.__local if strict else True))

    def is_matched(self):
        return bool(self.__remote and self.__local)

    def is_edited(self):
        return self.is_matched() and self.local.get_edited_time() > self.remote.get_create_time()

    @property
    def remote(self):
        return self.__remote

    @property
    def local(self):
        return self.__local

    @property
    def data(self):
        return self.__remote or self.__local

    @property
    def id(self):
        return self.__remote and self.__remote.get_id()

    @property
    def title(self):
        return self.data.get_title()

    @property
    def hash(self):
        return self.data.get_hash()

    @property
    def ratio(self):
        return self.data.get_ratio()

    @property
    def size(self):
        return self.data.get_size()

    def get_capture_time(self):
        return self.data.get_capture_time()

    def assign_local(self, other):
        assert isinstance(other, MatchedPhoto), type(other)
        self.__local = other.local

    def similar_to(self, other):
        return self.hash.similar_to(other.hash)

    def wximage(self, w, h):
        return self.data.wximage(w, h)

    def delete(self):
        if self.__remote:
            self.__remote.delete()
